/*
 *  Copyright © 2018 - 2021 xulianqiang90@163.com. All Rights Reserved.
 */

package io.light.frame.dal.mybatis.generator.core.domain.clazz;

import io.light.frame.dal.mybatis.generator.core.context.GenerationContext;
import io.light.frame.dal.mybatis.generator.core.domain.mapper.MapperFunc;
import io.light.frame.dal.mybatis.generator.core.exceptions.MybatisGenException;
import io.light.frame.dal.mybatis.generator.core.util.GenToolKit;
import io.light.frame.dal.mybatis.generator.core.util.StatefulSet;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.regex.Pattern;

/**
 * Class definition
 *
 * @author Ivan
 * @version 1.0.0
 * @date 2021-05-15 08:35
 */
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public final class Clazz {
    private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^[A-Za-z.]+$");
    private final String pkg;
    /**
     * Display like {@link Class#getCanonicalName()}
     */
    @EqualsAndHashCode.Include
    private final String name;
    /**
     * Display like {@link Class#getSimpleName()}
     */
    private final String simpleName;
    private ClazzMode mode;
    private final Class<?> realClass;
    private final StatefulSet<Clazz> interfaces = new StatefulSet<>();
    private final StatefulSet<JavaKeyword> keywords = new StatefulSet<>();
    @EqualsAndHashCode.Include
    private final StatefulSet<Clazz> genericTypes = new StatefulSet<>();
    private final StatefulSet<ClazzField> fields = new StatefulSet<>();
    private final StatefulSet<ClazzMethod> methods = new StatefulSet<>();
    private final StatefulSet<Clazz> annotations = new StatefulSet<>();
    private final StatefulSet<Clazz> declaredClasses = new StatefulSet<>();
    private final StatefulSet<Clazz> imports = new StatefulSet<>();
    private final boolean primitive;
    private Clazz superClazz;
    private String comment;
    private final int arrDim;
    private volatile String label;

    public Clazz(String pkg, String simpleName, ClazzMode mode, JavaKeyword... keywords) {
        this(StringUtils.isNotBlank(pkg) ? pkg + "." + simpleName : simpleName, mode, (Clazz[]) null);
        GenToolKit.fillKeywords(this.keywords, keywords);
    }

    private Clazz(Class<?> realClass) {
        this(realClass, (Clazz[]) null);
    }

    private Clazz(Class<?> realClass, Class<?>... genericTypes) {
        this(realClass, (Clazz[]) null);
        addGenerics(genericTypes);
    }

    private Clazz(Class<?> realClass, Clazz... genericTypes) {
        this(realClass.getCanonicalName(), ClazzMode.of(realClass), genericTypes);
    }

    private Clazz(String fullName, ClazzMode mode) {
        this(fullName, mode, (Clazz[]) null);
    }

    private Clazz(String fullName, ClazzMode mode, JavaKeyword... keywords) {
        this(fullName, mode, (Clazz[]) null);
        GenToolKit.fillKeywords(this.keywords, keywords);
    }

    private Clazz(String fullName, ClazzMode mode, Class<?>... genericTypes) {
        this(fullName, mode, (Clazz[]) null);
        addGenerics(genericTypes);
    }

    private Clazz(final String fullName, final ClazzMode expectMode, Clazz... genericTypes) {
        if (StringUtils.isBlank(fullName)) {
            throw new MybatisGenException("Clazz name cannot be empty");
        }
        String clazzName = fullName.trim();
        ClazzMode clazzMode = expectMode == null ? ClazzMode.UNKNOWN : expectMode;
        if (clazzName.equalsIgnoreCase("map")) {
            clazzName = "Map(String,Object)";
        }
        if (clazzName.equalsIgnoreCase("list")) {
            clazzName = "List(Object)";
        }
        if (clazzName.equals(MapperFunc.ENTITY_CLAZZ_NAME_ALIAS) &&
                GenerationContext.current(false) != null && GenerationContext.entityClazz() != null) {
            clazzName = GenerationContext.entityClazz().name;
        }
        if (clazzMode == ClazzMode.ARRAY && !fullName.endsWith("[]")) {
            clazzName = fullName + "[]";
        }
        if (clazzName.length() == 1) {
            clazzMode = ClazzMode.GENERIC_LABEL;
        }
        String declaredClassName = clazzName;
        int arrDim = 0;
        // handle annotation label
        if (clazzName.startsWith("@")) {
            clazzMode = ClazzMode.ANNOTATION_LABEL;
        }
        // handle generic label
        else if (clazzMode == ClazzMode.GENERIC_LABEL) {
            if (clazzName.length() != 1) {
                throw new MybatisGenException("Illegal generic label: " + fullName);
            }
            char c = clazzName.charAt(0);
            if (!(c == '?' || (c >= 'A' && c <= 'Z'))) {
                throw new MybatisGenException("Illegal generic label: " + fullName);
            }
        } else {
            // handle array
            if (clazzName.endsWith("[]")) {
                clazzMode = ClazzMode.ARRAY;
                while (StringUtils.isNotBlank(declaredClassName) && declaredClassName.endsWith("[]")) {
                    declaredClassName = declaredClassName.substring(0, declaredClassName.lastIndexOf("[]"));
                    arrDim++;
                }
            }
            // handle generic, like 'Abc(P1,P2)'
            if (!declaredClassName.startsWith("(") && declaredClassName.contains("(") && declaredClassName.endsWith(")")
                    && !declaredClassName.contains("()")) {
                clazzMode = ClazzMode.UNKNOWN;
                String generics = declaredClassName.substring(declaredClassName.indexOf("(") + 1,
                        declaredClassName.lastIndexOf(")"));
                declaredClassName = declaredClassName.substring(0, declaredClassName.indexOf("("));
                if (declaredClassName.equalsIgnoreCase("map")) {
                    declaredClassName = Map.class.getCanonicalName();
                }
                if (declaredClassName.equalsIgnoreCase("list")) {
                    declaredClassName = List.class.getCanonicalName();
                }
                for (String genericTypeName : generics.split(",")) {
                    if (StringUtils.isBlank(genericTypeName)) {
                        continue;
                    }
                    genericTypeName = genericTypeName.trim();
                    if (genericTypeName.length() == 1) {
                        addGenerics(Clazz.of(genericTypeName, ClazzMode.GENERIC_LABEL));
                    } else {
                        if (genericTypeName.equals(MapperFunc.ENTITY_CLAZZ_NAME_ALIAS) &&
                                GenerationContext.current(false) != null && GenerationContext.entityClazz() != null) {
                            addGenerics(GenerationContext.entityClazz());
                        } else {
                            addGenerics(Clazz.of(genericTypeName));
                        }
                    }
                }
            }
            // check declared class name
            if (!CLASS_NAME_PATTERN.matcher(declaredClassName).matches()) {
                throw new MybatisGenException("Illegal clazz name: " + fullName);
            }
        }
        Class<?> realClass = null;
        if (clazzMode != ClazzMode.ANNOTATION_LABEL && clazzMode != ClazzMode.GENERIC_LABEL) {
            try {
                realClass = GenToolKit.findClass(clazzName);
            } catch (Exception e) {
                // skip
            }
        }
        if (realClass != null) {
            if (clazzMode == ClazzMode.ARRAY) {
                this.primitive = false;
            } else {
                this.primitive = realClass.isPrimitive();
                clazzMode = ClazzMode.of(realClass);
                declaredClassName = realClass.getCanonicalName();
            }
        } else {
            this.primitive = false;
        }
        this.realClass = realClass;
        StringBuilder nameBuilder = new StringBuilder(declaredClassName);
        if (isArray()) {
            for (int i = 0; i < arrDim; i++) {
                nameBuilder.append("[]");
            }
        }
        this.name = nameBuilder.toString();
        this.mode = clazzMode;
        this.arrDim = arrDim;
        if (mode == ClazzMode.ANNOTATION_LABEL) {
            this.pkg = null;
            this.simpleName = this.name.substring(1);
        } else {
            this.pkg = this.name.contains(".") ? this.name.substring(0, this.name.lastIndexOf(".")) : null;
            this.simpleName = this.name.contains(".") ? this.name.substring(this.name.lastIndexOf(".") + 1) : this.name;
        }
        addGenerics(genericTypes);
        buildLabel();
    }

    public static Clazz of(Class<?> actualClass) {
        return new Clazz(actualClass);
    }

    public static Clazz of(Class<?> actualClass, Clazz... genericTypes) {
        return new Clazz(actualClass, genericTypes);
    }

    public static Clazz of(Class<?> actualClass, Class<?>... genericTypes) {
        return new Clazz(actualClass, genericTypes);
    }

    public static Clazz of(String fullName) {
        return new Clazz(fullName, ClazzMode.UNKNOWN);
    }

    public static Clazz of(String fullName, ClazzMode mode) {
        return new Clazz(fullName, mode);
    }

    public static Clazz of(String fullName, ClazzMode mode, Clazz... genericTypes) {
        return new Clazz(fullName, mode);
    }

    public static Clazz of(String fullName, ClazzMode mode, Class<?>... genericTypes) {
        return new Clazz(fullName, mode);
    }

    public Clazz addGenerics(Class<?>... generics) {
        Optional.ofNullable(generics).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .map(Clazz::of)
                .forEach(this.genericTypes::add)
        );
        return this;
    }

    public Clazz addGenerics(Clazz... generics) {
        Optional.ofNullable(generics).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .forEach(this.genericTypes::add)
        );
        return this;
    }

    public void setSuperClass(Class<?> superClass) {
        if (superClass != null) {
            setSuperClazz(Clazz.of(superClass));
        }
    }

    public void setSuperClazz(Clazz superClazz) {
        if (superClazz != null) {
            switch (mode) {
                case UNKNOWN:
                case CLASS:
                    if (superClazz.getMode() == ClazzMode.CLASS) {
                        this.superClazz = superClazz;
                        return;
                    }
                    if (superClazz.getMode() == ClazzMode.INTERFACE) {
                        addInterfaces(superClazz);
                        return;
                    }
                    break;
                case INTERFACE:
                    if (superClazz.getMode() == ClazzMode.INTERFACE) {
                        addInterfaces(superClazz);
                    }
                    break;
                default:
                    break;
            }
        }
    }

    public Clazz addInterfaces(Class<?>... interfaceClasses) {
        Optional.ofNullable(interfaceClasses).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .map(Clazz::of)
                .forEach(this.interfaces::add)
        );
        return this;
    }

    public Clazz addInterfaces(Clazz... interfaceClasses) {
        Optional.ofNullable(interfaceClasses).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .forEach(this.interfaces::add)
        );
        return this;
    }

    public Clazz addFields(ClazzField... fields) {
        Optional.ofNullable(fields).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .forEach(f -> {
                    f.setTarget(this);
                    this.fields.add(f);
                })
        );
        return this;
    }

    public Clazz addMethods(ClazzMethod... methods) {
        Optional.ofNullable(methods).ifPresent(arr -> Arrays.stream(arr)
                .filter(Objects::nonNull)
                .forEach(m -> {
                    m.setTarget(this);
                    this.methods.add(m);
                })
        );
        return this;
    }

    public Clazz addAnnotations(Class<? extends Annotation>... annotations) {
        GenToolKit.fillAnnotations(this.annotations, annotations);
        return this;
    }

    public Clazz addAnnotations(Clazz... annotations) {
        GenToolKit.fillAnnotations(this.annotations, annotations);
        return this;
    }

    private void addImport(ClazzField field) {
        addImport(field.getType());
        field.getAnnotations().forEach(this::addImport);
    }

    private void addImport(ClazzMethod method) {
        method.getParams().forEach(this::addImport);
        addImport(method.getReturnType());
        method.getAnnotations().forEach(this::addImport);
    }

    private void addImport(ClazzMethod.Param methodParam) {
        addImport(methodParam.getType());
        methodParam.getAnnotations().forEach(this::addImport);
    }

    private Clazz addImport(Clazz clazz) {
        if (clazz == null || clazz.isPrimitive()) {
            return this;
        }
        if (!clazz.getGenericTypes().isEmpty()) {
            clazz.getGenericTypes().forEach(this::addImport);
        }
        if (!clazz.getDeclaredClasses().isEmpty()) {
            clazz.getDeclaredClasses().forEach(this::addImport);
        }
        if (clazz.isArray()) {
            return this;
        }
        if (clazz.getMode() == ClazzMode.GENERIC_LABEL || clazz.getMode() == ClazzMode.ANNOTATION_LABEL) {
            return this;
        }
        String clazzName = clazz.getName();
        if (clazzName.startsWith("java.lang.") || "Object".equals(clazzName) || Objects.equals(pkg, clazz.getPkg())
                || clazzName.startsWith("@") || clazzName.contains("(")) {
            return this;
        }
        Class<?> arrItemType = GenToolKit.findClass(clazz.getName());
        if (arrItemType != null && arrItemType.isPrimitive()) {
            return this;
        }
        imports.add(clazz);
        return this;
    }

    public Clazz complete() {
        if (imports.isCompleted()) {
            return this;
        }
        addImport(superClazz);
        interfaces.forEach(this::addImport);
        declaredClasses.forEach(this::addImport);
        annotations.forEach(this::addImport);
        fields.forEach(field -> {
            if (field == null) {
                return;
            }
            Assert.notNull(field.getName(), String.format("Class '%s' missing field name", name));
            Assert.isTrue(field.getType() != null, String.format("Field '%s#%s' missing java type",
                    name, field.getName()));
            addImport(field);
        });
        methods.forEach(method -> {
            if (method == null) {
                return;
            }
            Assert.notNull(method.getName(), String.format("Class '%s' missing method name", name));
            addImport(method);
        });
        // rebuild label
        buildLabel();
        // clean imports
        List<String> importCleaner = new ArrayList<>();
        imports.removeIf(clazz -> {
            if (importCleaner.contains(clazz.getName())) {
                return true;
            }
            importCleaner.add(clazz.getName());
            return false;
        });
        importCleaner.clear();
        // complete
        StatefulSet.markCompleted(this);

        return this;
    }

    private void buildLabel() {
        if (mode == ClazzMode.ANNOTATION || mode == ClazzMode.ANNOTATION_LABEL) {
            this.label = simpleName;
        }
        String label = !isArray() ? simpleName : simpleName.substring(0, simpleName.length() - arrDim * 2);
        StringBuilder labelBuilder = new StringBuilder(label);
        if (!genericTypes.isEmpty()) {
            labelBuilder.append("<");
            int i = 0;
            int max = genericTypes.size() - 1;
            for (Clazz t : genericTypes) {
                labelBuilder.append(t.getLabel());
                if (t.getMode() == ClazzMode.GENERIC_LABEL && t.getSuperClazz() != null) {
                    labelBuilder.append(" extends ").append(t.getSuperClazz().getLabel());
                }
                if (i < max) {
                    labelBuilder.append(", ");
                }
                i++;
            }
            labelBuilder.append(">");
        }
        if (mode == ClazzMode.ARRAY) {
            for (int i = 0; i < arrDim; i++) {
                labelBuilder.append("[]");
            }
        }
        this.label = labelBuilder.toString();
    }

    private void setLabel(String label) {
        // do nothing
    }

    public boolean isInterface() {
        return mode == ClazzMode.INTERFACE;
    }

    public boolean isAnnotation() {
        return mode == ClazzMode.ANNOTATION;
    }

    public boolean isArray() {
        return mode == ClazzMode.ARRAY;
    }

    public boolean isEnum() {
        return mode == ClazzMode.ENUM;
    }

    public Clazz asArray() {
        Clazz arrClazz = of(name + "[]");
        this.genericTypes.forEach(arrClazz::addGenerics);
        arrClazz.declaredClasses.addAll(this.declaredClasses);
        return arrClazz;
    }

    public Clazz apply(Clazz clazz) {
        setMode(clazz.getMode());
        keywords.addAll(clazz.getKeywords());
        setSuperClazz(clazz.getSuperClazz());
        addInterfaces(clazz.getInterfaces().toArray(new Clazz[0]));
        addAnnotations(clazz.getAnnotations().toArray(new Clazz[0]));
        addFields(clazz.getFields().toArray(new ClazzField[0]));
        addMethods(clazz.getMethods().toArray(new ClazzMethod[0]));
        setComment(clazz.getComment());
        return this;
    }

    @Override
    public String toString() {
        return name;
    }
}
